﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using System.Security;
using System.Net.Security;
using System.IO;

namespace Sayeet
{
    public delegate void Connected();
    public delegate void StateChangedEventHandler(XMPPConnection.ThreadState st);
    public delegate void RcvQueueUpdatedEventHandler(XMPPConnection conn);

    public delegate void StartTLSHandler();


    public class XMPPConnection
    {
        event Connected OnConnect;
        public event StateChangedEventHandler StateChanged;
        public event RcvQueueUpdatedEventHandler RcvQueueUpdated;

        private event StartTLSHandler StartTLSEvent;


        private AppData appData;
        private string address = null;
        private int port = 0;

        private Socket Socket;
        private Stream Stream = null;
        private NetworkStream NetStream = null;
        private SslStream SSLStream = null;

        private Thread sendThread = null;
        private Thread rcvThread = null;

        private Queue SyncdSendQueue = Queue.Synchronized(new Queue());
        private Queue SyncdRcvQueue = Queue.Synchronized(new Queue());

        private XmlWriter Writer = null;
        private XmlReader Reader = null;
        private XmlWriterSettings WriterSettings = null;
        private XmlReaderSettings ReaderSettings = null;

        public string ConnectionID = null;
        public string LoginID = "login0";

        public bool AcceptsPlaintext = false;
        public bool AcceptsDigest = false;

        public enum ThreadState
        {
            idle,
            starting,
            running,
            stopping
        }

        ThreadState threadState = ThreadState.idle;

        public enum Status
        {
            online,
            away,
            chat,
            dnd,
            xa,
            offline
        }

        public Status status = Status.offline;

        public XMPPConnection(AppData ad)
        {
            appData = ad;
            StartTLSEvent += new StartTLSHandler(StartTLS);
        }

        public string Address
        {
            get
            {
                return address;
            }
            set
            {
                address = value;
            }
        }

        public int Port
        {
            get
            {
                return port;
            }
            set
            {
                port = value;
            }
        }

        private void Start()
        {
            if (threadState == ThreadState.idle)
            {
                threadState = ThreadState.starting;
                sendThread = new Thread(new ThreadStart(SenderJob));
                rcvThread = new Thread(new ThreadStart(ReceiverJob));

                threadState = ThreadState.running;
                sendThread.Start();
                rcvThread.Start();

                OnStateChanged();
            }
        }

        private void Stop()
        {
            while (threadState != ThreadState.idle)
            {
                Thread.Sleep(100);
            }

            OnStateChanged();
        }

        private void CheckThreadState()
        {
            if (sendThread == null && rcvThread == null)
            {
                threadState = ThreadState.idle;
            }
        }

        private void SenderJob()
        {
            while (threadState == ThreadState.running)
            {
                try
                {
                    Writer.Flush();

                    if (SyncdSendQueue.Count != 0)
                    {
                        XMPPMessage message = (XMPPMessage)SyncdSendQueue.Dequeue();

                        appData.debugWindow.addText(message.stanza.ToString(), "-> ");
                        message.stanza.WriteTo(Writer);
                    }
                    Thread.Sleep(100);
                }
                catch (Exception)
                {
                    appData.debugWindow.addText("sender exception", "!!! ");
                }
            }
            sendThread = null;
            CheckThreadState();
        }

        private void ReceiverJob()
        {
            while (threadState == ThreadState.running)
            {
                try
                {
                    //appData.debugWindow.addText(Stream.ReadTimeout.ToString());
                    if (NetStream.DataAvailable && Reader.Read())
                    {
                        switch (Reader.NodeType)
                        {
                            case XmlNodeType.XmlDeclaration:
                                // TODO: set encoding;
                                break;

                            case XmlNodeType.Element:
                                switch (Reader.Name)
                                {
                                    case "stream:stream":
                                        string id = Reader.GetAttribute("id");
                                        ConnectionID = id;
                                        appData.debugWindow.addText("stream initialized", "+++ ");
                                        appData.debugWindow.addText("stream id: " + id, "+++ ");
                                        appData.Connected = true;
                                        break;

                                    #region No longer needed
                                    /// No longer needed
                                    /*
                                    case "stream:features":
                                        appData.debugWindow.addText("features");
                                        
                                        break;

                                    case "starttls":
                                        {
                                            appData.debugWindow.addText("starttls");

                                            XNamespace ns = "urn:ietf:params:xml:ns:xmpp-tls";
                                            XElement response = new XElement(ns + "starttls");
                                            //SyncdSendQueue.Enqueue(new XMPPMessage(response));
                                            //StartTLSEvent();
                                        }
                                        break;

                                    case "iq":
                                        string type = Reader.GetAttribute("type");
                                        appData.debugWindow.addText("iq: type: " + type);
                                        if (type == "result")
                                        {
                                            //Reader.Read();
                                            XElement query = (XElement)XElement.ReadFrom(Reader);
                                            appData.debugWindow.addText("query: " + query.ToString());
                                            //if (query.Element("digest").
                                        }
                                        break;

                                    case "query":
                                        if (Reader.HasAttributes)
                                        {
                                            switch (Reader.GetAttribute("xmlns"))
                                            {
                                                case "jabber:iq:auth":
                                                    appData.debugWindow.addText("auth response");
                                                    XmlReader subtreeReader = Reader.ReadSubtree();
                                                    subtreeReader.Read();
                                                    XElement response = (XElement)XElement.ReadFrom(subtreeReader);
                                                    appData.debugWindow.addText(response.ToString());
                                                    break;
                                                default:
                                                    break;
                                            }
                                            
                                        }
                                        break;

                                    default:
                                        {
                                            appData.debugWindow.addText("element: " + Reader.Name);
                                            //if (Reader.HasAttributes)
                                            //{
                                            //    for (int i = 0; i < Reader.AttributeCount; i++)
                                            //        appData.debugWindow.addText("  Attribute: " + Reader.GetAttribute(i));
                                            //}
                                            
                                            XElement e = (XElement)XElement.ReadFrom(Reader);
                                            if (e != null)
                                            {
                                                appData.debugWindow.addText(e.ToString());
                                                XMPPMessage m = new XMPPMessage(e);
                                                SyncdRcvQueue.Enqueue(m);
                                                OnRcvQueueUpdated();
                                            }
                                            else
                                                appData.debugWindow.addText("null");
                                        }
                                        break;
                                    */
                                    #endregion

                                    default:
                                        appData.debugWindow.addText("reading subtree");

                                        XmlReader subtreeReader = Reader.ReadSubtree();
                                        subtreeReader.Read();
                                        XElement e = (XElement)XElement.ReadFrom(subtreeReader);
                                        appData.debugWindow.addText(e.ToString());
                                        SyncdRcvQueue.Enqueue(new XMPPMessage(e));

                                        if (RcvQueueUpdated != null)
                                            RcvQueueUpdated(this);

                                        break;
                                }
                                break;
                            default:
                                break;
                        }
                    }
                }
                catch (Exception)
                {
                    appData.debugWindow.addText("receiver exception", "!!! ");
                }
            }
            
            rcvThread = null;
            CheckThreadState();
        }

        public void Connect(string addr, int p)
        {
            address = addr;
            port = p;

            Connect();
        }

        public void Connect()
        {
            IPAddress addr = Dns.GetHostAddresses(address)[0];
            IPEndPoint serverPoint = new IPEndPoint(addr, port);

            Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                Socket.Connect(serverPoint);
                NetStream = new NetworkStream(Socket, false);
                Stream = NetStream;
                SendSessionOpening();

                ReaderSettings = new XmlReaderSettings();
                //ReaderSettings.ConformanceLevel = ConformanceLevel.Fragment;
                ReaderSettings.ValidationType = ValidationType.None;

                Reader = XmlReader.Create(Stream, ReaderSettings);

                WriterSettings = new XmlWriterSettings();
                WriterSettings.ConformanceLevel = ConformanceLevel.Fragment;
                WriterSettings.Encoding = new UTF8Encoding(false);

                Writer = XmlWriter.Create(Stream, WriterSettings);


                if (threadState == ThreadState.idle)
                {
                    Start();
                }

                // TODO: start TLS

                //StartTLS();


            }
            catch (Exception)
            {
                appData.debugWindow.addText("connect exception", "!!! ");
            }

            // dummy text message
            XMPPMessage m = new XMPPMessage();
            m.BuildMessage("me", "you", "hello");
            //m.BuildOpening(address);
            //SyncdSendQueue.Enqueue(m);


            //	Send:<iq type='get' to='myjabber.net' id='MX_7'>
            //			<query xmlns='jabber:iq:auth'><username>gnauck</username></query>
            //		 </iq>
            //	Recv:<iq type="result" id="MX_7"><query xmlns="jabber:iq:auth"><username>gnauck</username><password/><digest/><resource/></query></iq> 
            //
            //	Send:<iq type='set' id='mx_login'><query xmlns='jabber:iq:auth'><username>gnauck</username><digest>27c05d464e3f908db3b2ca1729674bfddb28daf2</digest><resource>Office</resource></query></iq>
            //	Recv:<iq id="mx_login" type="result"/> 

            

        }

        public void LogIn()
        {

            XNamespace auth = "jabber:iq:auth";
            string p = "";
            string type = "";
            if (AcceptsDigest)
            {
                p = Sayeet.Properties.Settings.Default.Password;
                System.Security.Cryptography.SHA1 sha = System.Security.Cryptography.SHA1.Create();
                byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ConnectionID + Sayeet.Properties.Settings.Default.Password));

                StringBuilder sb = new StringBuilder();
                foreach (byte b in hash)
                {
                    sb.Append(b.ToString("x2"));
                }

                p = sb.ToString();
                type = "digest";
            }
            else if (AcceptsPlaintext)
            {
                p = Sayeet.Properties.Settings.Default.Password;
                type = "password";
            }
            else
            {
                appData.debugWindow.addText("No available login methods", "!!! ");
                Disconnect();
            }
            XElement e = new XElement("iq",
                new XAttribute("type", "set"),
                new XAttribute("id", LoginID),
                new XElement(auth + "query",
                    new XElement(auth + "username", Sayeet.Properties.Settings.Default.Login),
                    new XElement(auth + type, p),
                    new XElement(auth + "resource", "Sayeet")
                    )
                );

            XMPPMessage loginRequest = new XMPPMessage(e);
            SyncdSendQueue.Enqueue(loginRequest);
        }

        private void StartTLS()
        {
            Reader = null;
            Writer = null;

            Stream.Close();
            Stream = null;
            NetStream = null;

            NetStream = new NetworkStream(Socket, false);

            System.Security.Authentication.SslProtocols protocol = System.Security.Authentication.SslProtocols.Tls;
            SSLStream = new SslStream(NetStream, true);
            try
            {
                SSLStream.AuthenticateAsClient(Address, null, protocol, true);
            }
            catch (Exception e)
            {
                appData.debugWindow.addText("ssl exception: " + e.Message);
            }
            Stream = SSLStream;
            Writer = XmlWriter.Create(Stream, WriterSettings);
            Reader = XmlReader.Create(Stream, ReaderSettings);
        }

        private void SendSessionOpening()
        {
            string header = BuildSessionOpening();
            UTF8Encoding e = new UTF8Encoding(false);

            byte[] h = e.GetBytes(header);
            NetStream.Write(h, 0, h.Length);
            NetStream.Flush();
        }

        private void ReadSessionOpening(NetworkStream ns)
        {
            byte[] bytes = new byte[1024];
            byte b;
            int i = 0;

            while ((b = (byte)ns.ReadByte()) != '>')
            {
                bytes[i] = b;
                i++;
            }
            bytes[i] = (byte)'>';
            UTF8Encoding e = new UTF8Encoding(false);


            appData.debugWindow.addText(e.GetString(bytes));
        }

        public void Disconnect()
        {
            threadState = ThreadState.stopping;
            if (Socket.Connected)
            {
                try
                {
                    string tail = BuildSessionClosing();
                    byte[] t = Encoding.UTF8.GetBytes(tail);
                    NetStream.Write(t, 0, t.Length);
                }
                catch (Exception)
                {

                }
                finally
                {
                    try
                    {
                        Socket.Disconnect(false);

                    }
                    catch (Exception)
                    {
                    }
                    finally
                    {
                        Stop();

                        Writer = null;
                        Reader = null;
                        NetStream = null;
                        Socket = null;
                    }
                }
            }
        }

        internal string BuildSessionOpening()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<?xml version='1.0' ?>");
            sb.Append("<stream:stream ");
            sb.Append(" to='" + address + "'");
            sb.Append(" version='1.0'");
            sb.Append(" xmlns='jabber:client'");
            sb.Append(" xmlns:stream='http://etherx.jabber.org/streams' ");
            sb.Append(">");

            return sb.ToString();
        }

        internal string BuildSessionClosing()
        {
            return "</stream>";
        }

        protected void OnStateChanged()
        {
            if (StateChanged != null)
                StateChanged(threadState);
        }

        protected void OnRcvQueueUpdated()
        {
            if (RcvQueueUpdated != null)
                RcvQueueUpdated(this);
        }

        public void Send(XMPPMessage message)
        {
            SyncdSendQueue.Enqueue(message);
        }

        public XMPPMessage GetMessage()
        {
            lock (this)
            {
                if (SyncdRcvQueue.Count > 0)
                    return SyncdRcvQueue.Dequeue() as XMPPMessage;
                return null;
            }
        }
    }
}
